// This is an open source non-commercial project. Dear PVS-Studio, please check it.
// PVS-Studio Static Code Analyzer for C, C++ and C#: http://www.viva64.com

// ReSharper disable CheckNamespace
// ReSharper disable ClassNeverInstantiated.Global
// ReSharper disable CommentTypo
// ReSharper disable IdentifierTypo
// ReSharper disable InconsistentNaming
// ReSharper disable StringLiteralTypo
// ReSharper disable UnusedParameter.Local

/*
 * Ars Magna project, http://arsmagna.ru
 */

#region Using directives

using System;
using System.Drawing;
using System.Drawing.Drawing2D;

using static AM.Drawing.QRCoding.ArtQRCode;
using static AM.Drawing.QRCoding.QRCodeGenerator;

#endregion

#nullable enable

namespace AM.Drawing.QRCoding;

/// <summary>
///
/// </summary>
[System.Runtime.Versioning.SupportedOSPlatform ("windows")]
public class ArtQRCode
    : AbstractQRCode,
    IDisposable
{
    #region Construction

    /// <summary>
    /// Constructor without params to be used in COM Objects connections
    /// </summary>
    public ArtQRCode()
    {
        // пустое тело конструктора
    }

    /// <summary>
    /// Creates new ArtQrCode object
    /// </summary>
    /// <param name="data">QRCodeData generated by the QRCodeGenerator</param>
    public ArtQRCode
        (
            QRCodeData data
        )
        : base (data)
    {
        // пустое тело конструктора
    }

    #endregion

    /// <summary>
    /// Renders an art-style QR code with dots as modules. (With default settings: DarkColor=Black, LightColor=White, Background=Transparent, QuietZone=true)
    /// </summary>
    /// <param name="pixelsPerModule">Amount of px each dark/light module of the QR code shall take place in the final QR code image</param>
    /// <returns>QRCode graphic as bitmap</returns>
    public Bitmap GetGraphic
        (
            int pixelsPerModule
        )
    {
        return GetGraphic (pixelsPerModule, Color.Black, Color.White, Color.Transparent);
    }

    /// <summary>
    /// Renders an art-style QR code with dots as modules and a background image (With default settings: DarkColor=Black, LightColor=White, Background=Transparent, QuietZone=true)
    /// </summary>
    /// <param name="backgroundImage">A bitmap object that will be used as background picture</param>
    /// <returns>QRCode graphic as bitmap</returns>
    public Bitmap GetGraphic
        (
            Bitmap? backgroundImage = null
        )
    {
        return GetGraphic (10, Color.Black, Color.White, Color.Transparent, backgroundImage: backgroundImage);
    }

    /// <summary>
    /// Renders an art-style QR code with dots as modules and various user settings
    /// </summary>
    /// <param name="pixelsPerModule">Amount of px each dark/light module of the QR code shall take place in the final QR code image</param>
    /// <param name="darkColor">Color of the dark modules</param>
    /// <param name="lightColor">Color of the light modules</param>
    /// <param name="backgroundColor">Color of the background</param>
    /// <param name="backgroundImage">A bitmap object that will be used as background picture</param>
    /// <param name="pixelSizeFactor">Value between 0.0 to 1.0 that defines how big the module dots are. The bigger the value, the less round the dots will be.</param>
    /// <param name="drawQuietZones">If true a white border is drawn around the whole QR Code</param>
    /// <param name="quietZoneRenderingStyle">Style of the quiet zones</param>
    /// <param name="backgroundImageStyle">Style of the background image (if set). Fill=spanning complete graphic; DataAreaOnly=Don't paint background into quietzone</param>
    /// <param name="finderPatternImage">Optional image that should be used instead of the default finder patterns</param>
    /// <returns>QRCode graphic as bitmap</returns>
    public Bitmap GetGraphic
        (
            int pixelsPerModule,
            Color darkColor,
            Color lightColor,
            Color backgroundColor,
            Bitmap? backgroundImage = null,
            double pixelSizeFactor = 0.8,
            bool drawQuietZones = true,
            QuietZoneStyle quietZoneRenderingStyle = QuietZoneStyle.Dotted,
            BackgroundImageStyle backgroundImageStyle = BackgroundImageStyle.DataAreaOnly,
            Bitmap? finderPatternImage = null
        )
    {
        if (pixelSizeFactor > 1)
        {
            throw new Exception ("The parameter pixelSize must be between 0 and 1. (0-100%)");
        }

        var pixelSize = (int)Math.Min (pixelsPerModule, Math.Floor (pixelsPerModule / pixelSizeFactor));
        var numModules = QrCodeData!.ModuleMatrix!.Count - (drawQuietZones ? 0 : 8);
        var offset = drawQuietZones ? 0 : 4;
        var size = numModules * pixelsPerModule;
        var bitmap = new Bitmap (size, size);

        using var graphics = Graphics.FromImage (bitmap);
        using var lightBrush = new SolidBrush (lightColor);
        using var darkBrush = new SolidBrush (darkColor);

        // make background transparent
        using (var brush = new SolidBrush (backgroundColor))
            graphics.FillRectangle (brush, new Rectangle (0, 0, size, size));

        //Render background if set
        if (backgroundImage != null)
        {
            if (backgroundImageStyle == BackgroundImageStyle.Fill)
            {
                graphics.DrawImage (Resize (backgroundImage, size)!, 0, 0);
            }
            else if (backgroundImageStyle == BackgroundImageStyle.DataAreaOnly)
            {
                var bgOffset = 4 - offset;
                graphics.DrawImage
                    (
                        Resize (backgroundImage, size - 2 * bgOffset * pixelsPerModule)!,
                        0 + bgOffset * pixelsPerModule,
                        bgOffset * pixelsPerModule
                    );
            }
        }

        var darkModulePixel = MakeDotPixel (pixelsPerModule, pixelSize, darkBrush);
        var lightModulePixel = MakeDotPixel (pixelsPerModule, pixelSize, lightBrush);

        for (var x = 0; x < numModules; x += 1)
        {
            for (var y = 0; y < numModules; y += 1)
            {
                var rectangleF = new Rectangle (x * pixelsPerModule, y * pixelsPerModule, pixelsPerModule,
                    pixelsPerModule);

                var pixelIsDark = QrCodeData.ModuleMatrix[offset + y][offset + x];
                var solidBrush = pixelIsDark ? darkBrush : lightBrush;
                var pixelImage = pixelIsDark ? darkModulePixel : lightModulePixel;

                if (!IsPartOfFinderPattern (x, y, numModules, offset))
                {
                    if (drawQuietZones && quietZoneRenderingStyle == QuietZoneStyle.Flat &&
                        IsPartOfQuietZone (x, y, numModules))
                    {
                        graphics.FillRectangle (solidBrush, rectangleF);
                    }
                    else
                    {
                        graphics.DrawImage (pixelImage, rectangleF);
                    }
                }
                else if (finderPatternImage == null)
                {
                    graphics.FillRectangle (solidBrush, rectangleF);
                }
            }
        }

        if (finderPatternImage != null)
        {
            var finderPatternSize = 7 * pixelsPerModule;
            graphics.DrawImage (finderPatternImage,
                new Rectangle (0, 0, finderPatternSize, finderPatternSize));
            graphics.DrawImage (finderPatternImage,
                new Rectangle (size - finderPatternSize, 0, finderPatternSize, finderPatternSize));
            graphics.DrawImage (finderPatternImage,
                new Rectangle (0, size - finderPatternSize, finderPatternSize, finderPatternSize));
        }

        graphics.Save();

        return bitmap;
    }

    /// <summary>
    /// If the pixelSize is bigger than the pixelsPerModule or may end up filling the Module making a traditional QR code.
    /// </summary>
    /// <param name="pixelsPerModule">Pixels used per module rendered</param>
    /// <param name="pixelSize">Size of the dots</param>
    /// <param name="brush">Color of the pixels</param>
    private Bitmap MakeDotPixel
        (
            int pixelsPerModule,
            int pixelSize,
            SolidBrush brush
        )
    {
        // draw a dot
        var bitmap = new Bitmap (pixelSize, pixelSize);
        using (var graphics = Graphics.FromImage (bitmap))
        {
            graphics.FillEllipse (brush, new Rectangle (0, 0, pixelSize, pixelSize));
            graphics.Save();
        }

        var pixelWidth = Math.Min (pixelsPerModule, pixelSize);
        var margin = Math.Max ((pixelsPerModule - pixelWidth) / 2, 0);

        // center the dot in the module and crop to stay the right size.
        var cropped = new Bitmap (pixelsPerModule, pixelsPerModule);
        using (var graphics = Graphics.FromImage (cropped))
        {
            graphics.DrawImage (bitmap, new Rectangle (margin, margin, pixelWidth, pixelWidth),
                new RectangleF (((float)pixelSize - pixelWidth) / 2, ((float)pixelSize - pixelWidth) / 2, pixelWidth,
                    pixelWidth),
                GraphicsUnit.Pixel);
            graphics.Save();
        }

        return cropped;
    }


    /// <summary>
    /// Checks if a given module(-position) is part of the quietzone of a QR code
    /// </summary>
    /// <param name="x">X position</param>
    /// <param name="y">Y position</param>
    /// <param name="numModules">Total number of modules per row</param>
    /// <returns>true, if position is part of quiet zone</returns>
    private bool IsPartOfQuietZone
        (
            int x,
            int y,
            int numModules
        )
    {
        return
            x < 4 || //left
            y < 4 || //top
            x > numModules - 5 || //right
            y > numModules - 5; //bottom
    }

    /// <summary>
    /// Checks if a given module(-position) is part of one of the three finder patterns of a QR code
    /// </summary>
    /// <param name="x">X position</param>
    /// <param name="y">Y position</param>
    /// <param name="numModules">Total number of modules per row</param>
    /// <param name="offset">Offset in modules (usually depending on drawQuietZones parameter)</param>
    /// <returns>true, if position is part of any finder pattern</returns>
    private bool IsPartOfFinderPattern
        (
            int x,
            int y,
            int numModules,
            int offset
        )
    {
        var cornerSize = 11 - offset;
        var outerLimitLow = numModules - cornerSize - 1;
        var outerLimitHigh = outerLimitLow + 8;
        var invertedOffset = 4 - offset;

        return
            x >= invertedOffset && x < cornerSize && y >= invertedOffset &&
            y < cornerSize || //Top-left finder pattern
            x > outerLimitLow && x < outerLimitHigh && y >= invertedOffset &&
            y < cornerSize || //Top-right finder pattern
            x >= invertedOffset && x < cornerSize && y > outerLimitLow &&
            y < outerLimitHigh; //Bottom-left finder pattern
    }

    /// <summary>
    /// Resize to a square bitmap, but maintain the aspect ratio by padding transparently.
    /// </summary>
    /// <param name="image"></param>
    /// <param name="newSize"></param>
    /// <returns>Resized image as bitmap</returns>
    private Bitmap? Resize
        (
            Bitmap? image,
            int newSize
        )
    {
        if (image == null)
        {
            return null;
        }

        var scale = Math.Min ((float)newSize / image.Width, (float)newSize / image.Height);
        var scaledWidth = (int)(image.Width * scale);
        var scaledHeight = (int)(image.Height * scale);
        var offsetX = (newSize - scaledWidth) / 2;
        var offsetY = (newSize - scaledHeight) / 2;

        var scaledImage = new Bitmap (image, new Size (scaledWidth, scaledHeight));

        var bm = new Bitmap (newSize, newSize);

        using var graphics = Graphics.FromImage (bm);
        using var brush = new SolidBrush (Color.Transparent);
        graphics.FillRectangle (brush, new Rectangle (0, 0, newSize, newSize));

        graphics.InterpolationMode = InterpolationMode.High;
        graphics.CompositingQuality = CompositingQuality.HighQuality;
        graphics.SmoothingMode = SmoothingMode.AntiAlias;

        graphics.DrawImage (scaledImage, new Rectangle (offsetX, offsetY, scaledWidth, scaledHeight));

        return bm;
    }

    /// <summary>
    /// Defines how the quiet zones shall be rendered.
    /// </summary>
    public enum QuietZoneStyle
    {
        /// <summary>
        ///
        /// </summary>
        Dotted,

        /// <summary>
        ///
        /// </summary>
        Flat
    }

    /// <summary>
    /// Defines how the background image (if set) shall be rendered.
    /// </summary>
    public enum BackgroundImageStyle
    {
        /// <summary>
        ///
        /// </summary>
        Fill,

        /// <summary>
        ///
        /// </summary>
        DataAreaOnly
    }
}

/// <summary>
/// Вспомогательные методы для Art QR-кода.
/// </summary>
[System.Runtime.Versioning.SupportedOSPlatform ("windows")]
internal static class ArtQRCodeHelper
{
    /// <summary>
    /// Helper function to create an ArtQRCode graphic with a single function call
    /// </summary>
    /// <param name="plainText">Text/payload to be encoded inside the QR code</param>
    /// <param name="pixelsPerModule">Amount of px each dark/light module of the QR code shall take place in the final QR code image</param>
    /// <param name="darkColor">Color of the dark modules</param>
    /// <param name="lightColor">Color of the light modules</param>
    /// <param name="backgroundColor">Color of the background</param>
    /// <param name="eccLevel">The level of error correction data</param>
    /// <param name="forceUtf8">Shall the generator be forced to work in UTF-8 mode?</param>
    /// <param name="utf8BOM">Should the byte-order-mark be used?</param>
    /// <param name="eciMode">Which ECI mode shall be used?</param>
    /// <param name="requestedVersion">Set fixed QR code target version.</param>
    /// <param name="backgroundImage">A bitmap object that will be used as background picture</param>
    /// <param name="pixelSizeFactor">Value between 0.0 to 1.0 that defines how big the module dots are. The bigger the value, the less round the dots will be.</param>
    /// <param name="drawQuietZones">If true a white border is drawn around the whole QR Code</param>
    /// <param name="quietZoneRenderingStyle">Style of the quiet zones</param>
    /// <param name="backgroundImageStyle">Style of the background image (if set). Fill=spanning complete graphic; DataAreaOnly=Don't paint background into quietzone</param>
    /// <param name="finderPatternImage">Optional image that should be used instead of the default finder patterns</param>
    /// <returns>QRCode graphic as bitmap</returns>
    public static Bitmap GetQRCode
        (
            string plainText,
            int pixelsPerModule,
            Color darkColor,
            Color lightColor,
            Color backgroundColor,
            ECCLevel eccLevel,
            bool forceUtf8 = false,
            bool utf8BOM = false,
            EciMode eciMode = EciMode.Default,
            int requestedVersion = -1,
            Bitmap? backgroundImage = null,
            double pixelSizeFactor = 0.8,
            bool drawQuietZones = true,
            QuietZoneStyle quietZoneRenderingStyle = QuietZoneStyle.Flat,
            BackgroundImageStyle backgroundImageStyle = BackgroundImageStyle.DataAreaOnly,
            Bitmap? finderPatternImage = null
        )
    {
        using var qrGenerator = new QRCodeGenerator();
        using var qrCodeData = qrGenerator.CreateQrCode (plainText, eccLevel, forceUtf8, utf8BOM, eciMode, requestedVersion);
        using var qrCode = new ArtQRCode (qrCodeData);

        return qrCode.GetGraphic
            (
                pixelsPerModule,
                darkColor,
                lightColor,
                backgroundColor,
                backgroundImage,
                pixelSizeFactor,
                drawQuietZones,
                quietZoneRenderingStyle,
                backgroundImageStyle,
                finderPatternImage
            );
    }
}
